﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using TFMAPI;

namespace TwitterFollowersMonitor
{
    public partial class frmMain : Form
    {

        private const string NOTIFYICON_TOOLTIP_BASE = "Twitter Followers Monitor";

        //To signal an autorized form closing
        bool mustClose = false;

        //The accounts to monitor
        TwitterMonitorizedAccountsInfo accounts = new TwitterMonitorizedAccountsInfo();

        //The next Date the Twitter API quota will be reset and we will be able to make new calls (one hour before the launch time by default)
        DateTime NextTwitterAPICountReset = DateTime.Now.AddHours(-1);

        public frmMain()
        {
            InitializeComponent();
        }

        private void frmMain_Load(object sender, EventArgs e)
        {
            try
            {
                //Show app version in notify icon
                notifyIcon1.Text = NOTIFYICON_TOOLTIP_BASE + " v" + Application.ProductVersion;

                //Initialize Application Data
                accounts = TwitterMonitorizedAccountsInfo.Load();

                //If the file is empty or has empty accounts list then is equivalent to not existing
                //so we force the "Add new twitter account" dialog
                if (accounts.Accounts.Count == 0)
                {
                    throw new System.IO.FileNotFoundException();
                }

                LoadDropdownList();
                LaunchBackgroundUpdate();

            }
            catch (System.IO.FileNotFoundException )    //No data, therefore no defined accounts --> This is the first run
            {
                MessageBox.Show("This is the first time you run the app. In the next dialog you must add at least a valid Twitter account to monitor.", "First run", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                AddNewTwitterAccount();
                if (accounts.Accounts.Count > 0)
                {
                    LoadDropdownList();
                }
                else
                {
                    mustClose = true;
                    this.Close();   //End application if no account is added
                }

            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }


        private int countChecks = 0;
        /// <summary>
        /// Checks from Twitter if there is a new version of the App and, if true, offers the chance to download it.
        /// </summary>
        /// <remarks>Checks (more or less) only once every 24 hours (24hours*4 checkings/hour = 96</remarks>
        private void CheckIfNewerVersion()
        {
            countChecks++;
            if (countChecks == 96)
            {
                //restart the count
                countChecks = 0;
                TFMVersion currV = TFMVersion.GetLastVersionInfo();
                if (currV.LastVersion > currV.CurrentVersion)
                {
                    DialogResult res = MessageBox.Show(String.Format("A new version (v{0}) of the Twitter Followers Monitor app is available.\n\n Do you want to download it?\n\nIf YES, PLEASE CLOSE THE APP AND RUN IT AGAIN FROM THE START MENU!!!",
                                                        currV.CurrentVersion), "Version check", MessageBoxButtons.YesNo, MessageBoxIcon.Information);
                    if (res == DialogResult.Yes)
                    {
                        System.Diagnostics.Process.Start(currV.DownloadURL);
                    }
                }
            }
        }


#region "UI Logic"

        /// <summary>
        /// This method overloads the form's NotifyIcon ShowBalloon Method for taking into account the fact 
        /// that the workstation is locked or has been disconnected in a terminal Server session.
        /// </summary>
        /// <param name="timeOut"></param>
        /// <param name="tipTitle"></param>
        /// <param name="tipText"></param>
        /// <param name="tipIcon"></param>
        /// <remarks>If the workstation is locked the balloons are not shown beacuse in that case all of them will
        /// appear later (afer unlocking) one after the other in a row, causing a dreadful chain of messages to appear during a long period.</remarks>
        private void ShowBalloon(int timeOut, string tipTitle, string tipText, ToolTipIcon tipIcon)
        {
            if (!GlobalExceptionHandler.SessionLocked)
            {
                notifyIcon1.ShowBalloonTip(timeOut, tipTitle, tipText, tipIcon);
            }
        }
        
        /// <summary>
        /// This method checks for the maximum number of followers or accounts monitorized 
        /// and notifies the user if there are more than 500.000 followers combined or more than 25 accounts.
        /// </summary>
        /// <remarks>
        /// The 500.000 followers /25 accounts limit is an estimated one due to the limit of 150 anonymous calls per hour to the Twitter API
        /// If the combined number of followers of the monitorized accounts is higher than this number probably the API limit will not be enough
        /// to recover all the changes in the followers' list.
        /// </remarks>
        private void CheckMaximumNumberOfFollowers()
        {
            if (accounts.Accounts.Count > 25)
            {
                ShowBalloon(20000, "Warning!",
                    @"Warning! The total number of accounts you're trying to monitorize is more than 25. 
Due to the limitations of the anonymous Twitter API calls it's advisable to monitor less accounts.
Please, REMOVE ONE OR MORE ACCOUNTS.", ToolTipIcon.Warning);
                return;
            }

            long followersSum = 0;
            foreach (KeyValuePair<string, TwitterUserInfo> item in accounts.Accounts)
            {
                followersSum += item.Value.followers_count;
            }
            if (followersSum > 500000)
            {
                ShowBalloon(20000, "Warning!",
                    @"Warning! The total number of followers you're trying to monitorize is more than 500.000. 
Due to the limitations of the anonymous Twitter API calls it's advisable to monitor less followers.
Please, REMOVE ONE OR MORE ACCOUNTS.", ToolTipIcon.Warning);
            }
        }

        //Loads or reloads the account selectionlist
        private void LoadDropdownList()
        {
            cbAccounts.Items.Clear();
            cbAccounts.Items.AddRange(accounts.Accounts.Keys.ToArray<string>());
            cbAccounts.SelectedIndex = 0;
        }

        private void ReloadHistoryGrid()
        {
            //Order history to show more recent events first
            var fh = from tubi in accounts.Accounts[cbAccounts.SelectedItem.ToString()].FollowersHistory
                     orderby tubi.eventdate descending
                     select tubi;
            if (fh.Count<TwitterUserBasicInfo>() > 0)
            {
                bsFollowersInfo.DataSource = fh;
            }
            else
            {
                bsFollowersInfo.DataSource = null;
            }
        }

        /// <summary>
        /// Shows the dialog for adding a new account, and adds the new one (if any) to the list of monitorized accounts
        /// </summary>
        /// <returns>Returns true if a new user was addded or false if not</returns>
        private bool AddNewTwitterAccount()
        {
            AddTwitterUserDlg dlg = new AddTwitterUserDlg();
            dlg.ShowDialog(this);
            if (dlg.DialogResult == DialogResult.OK)
            {
                if (dlg.resTUI != null)
                {
                    try
                    {
                        accounts.Accounts.Add(dlg.resTUI.screen_name, dlg.resTUI);
                        accounts.Save();
                        LaunchBackgroundUpdate();   //Update info about followers
                    }
                    catch(ArgumentException)    //The account already exists in the list
                    {
                        MessageBox.Show( String.Format("The Twitter account \"{0}\" is already being monitored.", dlg.resTUI.screen_name), "Add account", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    }
                }
            }

            return dlg.DialogResult == DialogResult.OK;
        }

        //Displays info about the remaining Twitter API calls
        private void DisplayAPILimitInfo()
        {
            rate_limit_status currentRate = TwitterAPIWrapper.GetTwitterAPIQuota(); //This call does not count for the API calls limit.
            lblStatus.Text = String.Format("{0} of {1} API calls remaining until {2}", currentRate.remaining_hits, currentRate.hourly_limit, currentRate.resetTime.ToLongTimeString());
        }

        //Shows in the status bar the changes in the number of followers since the last time the user opened the form
        //Returns false if no changes had been detected, true otherwise
        private bool NotifyChangesInFollowers()
        {
            string sAccountsWithChanges = "";
            foreach (KeyValuePair<string, TwitterUserInfo> item in accounts.Accounts)
            {
                if (item.Value.newFollowersCount != 0 || item.Value.lostFollowersCount != 0)
                {
                    if (sAccountsWithChanges != "")
                        sAccountsWithChanges += ", ";

                    sAccountsWithChanges += String.Format("{0} (+{1},-{2})", item.Key, item.Value.newFollowersCount, item.Value.lostFollowersCount); //Shows the new and lost followers between parenthesis
                }
            }

            //Show the last check date in the notification area tooltip
            notifyIcon1.Text = NOTIFYICON_TOOLTIP_BASE + " v" + Application.ProductVersion + Environment.NewLine + "Last checked at " + DateTime.Now.ToLongTimeString();

            if (sAccountsWithChanges == "")
            {
                notifyIcon1.Icon = global::TwitterFollowersMonitor.Properties.Resources.TwitterFollowerMonitor;
                lblFollowersChanges.ForeColor = Color.Black;
                lblFollowersChanges.Text = String.Format("No changes detected in followers. Last checked at {0}.", DateTime.Now.ToLongTimeString());
                return false;
            }
            else
            {
                notifyIcon1.Icon = global::TwitterFollowersMonitor.Properties.Resources.TwitterFollowersMonitorChanges;
                lblFollowersChanges.ForeColor = Color.Red;
                //Change ", " by new line for the notification balloon
                lblFollowersChanges.Text = String.Format("Changes detected in: {0}. Last checked at {1}.", sAccountsWithChanges, DateTime.Now.ToLongTimeString());
                ShowBalloon(5000, "Changes detected in", sAccountsWithChanges.Replace(", ", Environment.NewLine), ToolTipIcon.Info);    //Show notification balloon
                return true;
            }
        }

        /// <summary>
        /// Runs the checking and updating process in the background
        /// </summary>
        public void LaunchBackgroundUpdate()
        {
            //If the Twitter API quota is eceeded, don't launch new checks until the reset time
            if (NextTwitterAPICountReset > DateTime.Now)
            {
                lblStatus.Text = "Twitter API quota exceeded. No new calls until " + NextTwitterAPICountReset.ToLongTimeString();
            }
            else
            {
                lblStatus.Text = "Updating followers info for all accounts";
                //Diable toolBar to avoid modifications in the monitored accounts and relaunching the update
                upperToolBar.Enabled = false;
                bwUpdateData.RunWorkerAsync(accounts);
            }
        }

        /// <summary>
        /// Updates the information about all the monitorized accounts and spots the differences
        /// </summary>
        /// <param name="tmai">The current collection of monitorized accounts</param>
        /// <returns>A new collection with all the information about the accounts updated</returns>
        /// <remarks>Possibly, this method would be interesting to be moved to an independent class/file to separate this huge logic from the UI.
        /// However, by now, it keeps some ties to the UI, like the NextTwitterAPICountReset var to keep the next Twitter API reset time. Must refactor in the future.</remarks>
        public TwitterMonitorizedAccountsInfo UpdateAccountsInfo(TwitterMonitorizedAccountsInfo tmai)
        {
            TwitterMonitorizedAccountsInfo newAccountData = new TwitterMonitorizedAccountsInfo();

            foreach (KeyValuePair<string, TwitterUserInfo> item in tmai.Accounts)
            {
                TwitterUserInfo tui = item.Value;   //Current account info
                TwitterUserInfo tuiUpdated = null;  //Will handle the updated account info

                if (NextTwitterAPICountReset > DateTime.Now)    //If the Twitter API is exhausted then just add the account and move to the next (essentially do nothing for the rest of the loop)
                {
                    newAccountData.Accounts.Add(item.Key, tui);
                    continue;
                }

                try
                {
                    //Try to update the information for the current user from Twitter API
                    tuiUpdated = TwitterAPIWrapper.GetTwitterUserInfo(tui.screen_name);
                    //In any case update the information about the current user in the TwitterMonitorizedAccountsInfo class
                    newAccountData.Accounts.Add(item.Key, tuiUpdated);
                }
                catch (QuotaExceededException ex)
                {
                    //If Twitter API quota exceeded, then just add the current account to the updated info (we can't get new info right now) and skip the rest of the loop
                    newAccountData.Accounts.Add(item.Key, tui);
                    NextTwitterAPICountReset = ex.NextAvailableTime;   //This will prevent new API calls until the next call count reset
                    continue;   //jump to the next loop iteration
                }

                //Set the same followers history in order to save later
                tuiUpdated.FollowersHistory = tui.FollowersHistory;
                //The same Followers info initially
                tuiUpdated.Followers = tui.Followers;
                tuiUpdated.newFollowersCount = tui.newFollowersCount;   //This info is needed to preserve new & lost followers count info between calls while the current user doesn't open the UI
                tuiUpdated.lostFollowersCount = tui.lostFollowersCount;

                //If the number of followers does not match... or if the current user has no information available about followers (first time it's been here)
                if ((tui.followers_count != tuiUpdated.followers_count && tuiUpdated.followers_count > 0) || tui.Followers == null)
                {
                    FollowersInfo currentFollowers = null;
                    try
                    {
                        //...get the new followers list... (can be a long task)
                        currentFollowers = TwitterAPIWrapper.GetUserFollowersInfo(tui.screen_name);
                        //anyway the updated user information must get the new full list of followers ids
                        tuiUpdated.Followers = currentFollowers;
                    }
                    catch (QuotaExceededException ex)
                    {
                        NextTwitterAPICountReset = ex.NextAvailableTime;   //This will prevent new API calls until the next call count reset
                        //Remove the account form the list and add it again from the original list
                        newAccountData.Accounts.Remove(item.Key);
                        newAccountData.Accounts.Add(item.Key, item.Value);
                        continue;   //jump to the next loop iteration
                    }

                    //If current user followers list is a null, just save the initial followers information
                    //but don't consider the current followers as new 
                    //this will only happen normally on accounts with no followers, although it could lead to a new Twitter account first follower to be unnotified too
                    if (tui.Followers != null)
                    {
                        //...spot the differences (new followers and lost followers)...
                        IEnumerable<long> lostFollowers = tui.Followers.ids.Except(currentFollowers.ids).ToList<long>();    //convert to list to allow for changes in the var
                        IEnumerable<long> newFollowers = currentFollowers.ids.Except(tui.Followers.ids).ToList<long>();     //convert to list to allow for changes in the var

                        //...update the basic information for each lost and new followers
                        foreach (long followerId in lostFollowers)
                        {
                            //If the Twitter API is not available to get the lost follower info then add it again to the followers list to keep track of it later
                            if (NextTwitterAPICountReset > DateTime.Now)    //This var is set if only there is an exception (below)
                            {
                                tuiUpdated.Followers.ids.Add(followerId);
                                continue;
                            }

                            TwitterUserBasicInfo fbi = null;
                            tuiUpdated.lostFollowersCount++;
                            try
                            {
                                fbi = new TwitterUserBasicInfo(TwitterAPIWrapper.GetTwitterUserInfo(followerId));
                            }
                            catch (QuotaExceededException ex)
                            {
                                //If quota is exceeded then Add the remaining lost accounts to the Followers list so that they will be spotted the next time
                                NextTwitterAPICountReset = ex.NextAvailableTime;   //This will prevent new API calls until the next call count reset
                                tuiUpdated.lostFollowersCount--;    //We don't take into account this lost follower (we don't have the information about it
                                continue;   //jump to the next loop iteration
                            }
                            catch (InexistentTwitterUserException) //If the user does not exist that means that the follower deleted the account from Twitter
                            {
                                fbi = new TwitterUserBasicInfo();
                                fbi.name = "Deleted account - This user left Twitter";
                            }
                            catch (ProtectedUserInformationException)   //The user info can not be accessed because it's password protected
                            {
                                fbi = new TwitterUserBasicInfo();
                                fbi.name = "Protected account - Info not available";
                            }
                            fbi.NewUser = false;
                            fbi.eventdate = DateTime.Now;
                            tuiUpdated.FollowersHistory.Add(fbi);
                        }

                        foreach (long followerId in newFollowers)
                        {
                            //If the Twitter API is not available to get the new follower info then remove it from the followers list to keep track of it later
                            if (NextTwitterAPICountReset > DateTime.Now)    //This var is set only if there is an exception (below)
                            {
                                tuiUpdated.Followers.ids.Remove(followerId);
                                continue;
                            }

                            tuiUpdated.newFollowersCount++;
                            TwitterUserBasicInfo fbi = null;
                            try
                            {
                                fbi = new TwitterUserBasicInfo(TwitterAPIWrapper.GetTwitterUserInfo(followerId));
                            }
                            catch (QuotaExceededException ex)
                            {
                                //If quota is exceeded then Add the remaining lost accounts to the Followers list so that they will be spotted the next time
                                NextTwitterAPICountReset = ex.NextAvailableTime;   //This will prevent new API calls until the next call count reset
                                tuiUpdated.newFollowersCount--;    //We don't take into account this lost follower (we don't have the information about it
                                continue;   //jump to the next loop iteration
                            }
                            catch (InexistentTwitterUserException) //If the user does not exist that means that the follower deleted the account from Twitter (This should not happen normally in a new account, but is possible anyway)
                            {
                                fbi = new TwitterUserBasicInfo();
                                fbi.name = "Deleted account - This user left Twitter";
                            }
                            catch (ProtectedUserInformationException)   //The user info can not be accessed because it's password protected
                            {
                                fbi = new TwitterUserBasicInfo();
                                fbi.name = "Protected account - Info not available";
                            }
                            fbi.NewUser = true;
                            fbi.eventdate = DateTime.Now;
                            tuiUpdated.FollowersHistory.Add(fbi);
                        }
                    }
                }
            }

            //Save the accounts info
            newAccountData.Save();

            //Check if there is a new version of the App
            CheckIfNewerVersion();

            return newAccountData;
        }
#endregion

#region "Events"

        //Add a new account to monitor
        private void cmdAddAccount_Click(object sender, EventArgs e)
        {
            if (AddNewTwitterAccount())
            {
                LoadDropdownList();
                cbAccounts.SelectedIndex = cbAccounts.Items.Count - 1;
            }
            DisplayAPILimitInfo();
        }

        private void cbAccounts_SelectedIndexChanged(object sender, EventArgs e)
        {
            ctlAccountInfo.DisplayUserInfo(accounts.Accounts[cbAccounts.SelectedItem.ToString()]);
            ReloadHistoryGrid();
            ctlAccountInfo.Visible = true;
            ctlAccountInfo.Focus();
        }

        //Remove account form the list of monitorized accounts
        private void cmdRemoveAccount_Click(object sender, EventArgs e)
        {
            DialogResult res = MessageBox.Show(String.Format("Are you sure that you want to remove the account \"{0}\" from the list of monitorized accounts.", cbAccounts.SelectedItem.ToString()), 
                                    "Remove account", MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);

            if (res != DialogResult.OK)
                return;

            //If OK is pressed then remove the account from the list and stop monitoring its followers
            accounts.Accounts.Remove(cbAccounts.SelectedItem.ToString());
            accounts.Save();
            cbAccounts.Items.Remove(cbAccounts.SelectedItem);
            if (accounts.Accounts.Count > 0)
                cbAccounts.SelectedIndex = 0;
            else  //If there are not accounts left, then ask for a new one
            {
                ctlAccountInfo.Visible = false;
                MessageBox.Show("In order to use the tool, you'll need to add at least one new Twitter account.", "No accounts", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                AddNewTwitterAccount();
                if (accounts.Accounts.Count > 0)
                {
                    LoadDropdownList();
                }
                else
                    this.Close();   //End application if no account is added
            }
        }

///////////////////////////////////////////////////////////////////////////////
// BackgroundWorker worker events
///////////////////////////////////////////////////////////////////////////////

        private void bwUpdateData_DoWork(object sender, DoWorkEventArgs e)
        {
            e.Result = UpdateAccountsInfo((TwitterMonitorizedAccountsInfo) e.Argument);
        }

        private void bwUpdateData_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            //Re-enable the toolbar
            upperToolBar.Enabled = true;

            if (e.Error != null) //An error has happened!!
            {
                lblFollowersChanges.ForeColor = Color.Red;

                Type t = e.Error.GetType();
                if (t == typeof(InexistentTwitterUserException))
                {
                    lblFollowersChanges.Text = String.Format("This Twitter account does not exist: {0}. Please remove it from the list.", e.Error.Message);
                }
                else
                {
                    if (t == typeof(QuotaExceededException))
                    {
                        lblFollowersChanges.Text = String.Format("The current Twitter API calls hourly quota has been exceeded.");
                    }
                    else
                    {
                        lblFollowersChanges.Text = String.Format("Error: {0}", e.Error.Message);
                    }
                }
            }
            else //Success updating
            {
                if (NextTwitterAPICountReset > DateTime.Now)
                    lblFollowersChanges.Text = String.Format("The current Twitter API calls hourly quota has been exceeded.");

                accounts = e.Result as TwitterMonitorizedAccountsInfo;

                if (NotifyChangesInFollowers()) cbAccounts_SelectedIndexChanged(this, null);    //Reload current selected user info and followers history
            }

            DisplayAPILimitInfo();
            CheckMaximumNumberOfFollowers();
        }

///////////////////////////////////////////////////////////////////////////////
// END OF BackgroundWorker worker events
///////////////////////////////////////////////////////////////////////////////

        //Used for the update button and the update timer
        private void cmd_tmr_Update_Click(object sender, EventArgs e)
        {
            LaunchBackgroundUpdate();
        }

        private void lnkScreenName_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            LinkLabel lnk = (LinkLabel)sender;
            System.Diagnostics.Process.Start("http://twitter.com/" + lnk.Text);
        }

        private void cmdExit_Click(object sender, EventArgs e)
        {
            //Signal that we want to really close the program
            mustClose = true;
            this.Close();
        }

        private void frmMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (e.CloseReason == CloseReason.UserClosing && mustClose == false)
            {
                this.Hide();
                e.Cancel = true;
            }
        }

        private void notifyIcon1_Click(object sender, EventArgs e)
        {
            this.Show();
            this.Activate();
        }

        private void frmMain_Shown(object sender, EventArgs e)
        {
            //Start hidden
            this.Hide();
        }

        private void frmMain_Activated(object sender, EventArgs e)
        {
            bool changed = false;
            //Each time the form is activated, reset the info for the changes in followers 
            foreach (KeyValuePair<string, TwitterUserInfo> item in accounts.Accounts)
            {
                if (item.Value.newFollowersCount != 0 || item.Value.lostFollowersCount != 0)
                {
                    item.Value.newFollowersCount = 0;
                    item.Value.lostFollowersCount = 0;
                    changed = true;
                }
            }

            if (changed)
            {
                accounts.Save();
                notifyIcon1.Icon = global::TwitterFollowersMonitor.Properties.Resources.TwitterFollowerMonitor;
            }
        }

        private void cmdAbout_Click(object sender, EventArgs e)
        {
            AboutBox ab = new AboutBox();
            ab.ShowDialog();
        }

    }

#endregion


}
